Repository navigation
Conversation
|
Reminder, please take a look at this pr: @Abacn |
|
Load balancing out some reviews. R: @derrickaw |
|
Stopping reviewer notifications for this pull request: review requested by someone other than the bot, ceding control. If you'd like to restart, comment |
|
R: @ahmedabu98, can you take a quick peek at this to see if anything else is missing? Thanks |
ahmedabu98
left a comment
There was a problem hiding this comment.
Looks good from a skim, just consider adding a flattened output option.
Also please add a YAML IT
| .addNullableRowField("value_input", VALUE_SCHEMA) | ||
| .build(); | ||
|
|
||
| static final Schema CHANGE_STREAM_MUTATION_SCHEMA = |
There was a problem hiding this comment.
This output schema looks complex, which will be noticed when using it with YAML.
In the regular BigtableReadSchemaTransformProvider, we added an option to use a flattened schema (one output per entry, instead of per mutation). Consider doing something similar here
There was a problem hiding this comment.
flatten: true breaks row-level atomicity and loses intra-MutateRow entry ordering:
In Bigtable, a MutateRow applies a list of mutations atomically to a single row, and per the MutateRowRequest contract, entries within a MutateRow are applied in order so later entries mask earlier ones (for example, deleteRow() / deleteFamily() followed by setCell() in an atomic row overwrite).
ChangeStreamMutation intentionally groups all entries of a row mutation together to preserve both row-level atomicity and entry ordering. Splitting mutation.getEntries() into independent PCollection<Row> elements breaks row-level atomicity and—because all entries in the same ChangeStreamMutation share the exact same key, commit_timestamp, and token with no sequence index—loses intra-mutation ordering across any downstream shuffle (so a DeleteFamily followed by a SetCell in the same MutateRow can be replayed in reverse order downstream).
How to fix (two options):
- (Preferred) Remove
flattenmode: Always outputCHANGE_STREAM_MUTATION_SCHEMA(one BeamRowperChangeStreamMutationwith nestedentriesasARRAY<ROW>), which preserves both row-level atomicity and entry ordering. - If
flattenis needed for YAML convenience: Defaultflattentofalse(opt-in rather than opt-out), and add an.addInt32Field("entry_index")(the 0-based index withinmutation.getEntries()) toFLATTENED_CHANGE_STREAM_MUTATION_SCHEMAso downstream consumers can reconstruct the intra-mutation execution order.
|
cc @mutianf might be relevant for you |
I tried adding a Bigtable CDC YAML IT using a temporary change-stream-enabled table and a bounded end time, but the pipeline consistently times out in the YAML Direct/FnApi integration-test harness. Do you have a recommendation for how this integration test should be structured for a streaming CDC source? |
|
LGTM for yaml, @ahmedabu98 - did you want to take one more peek? Thanks |
Hmm why is it timing out? does this mean the |
|
waiting on author |
| @@ -259,6 +264,111 @@ def instance_prefix(instance): | |||
| return instance_id | |||
|
|
|||
|
|
|||
| @contextlib.contextmanager | |||
| def temp_bigtable_change_stream_table(project, prefix='yaml_bt_cdc_it_'): | |||
There was a problem hiding this comment.
Resource leak if setup or table cleanup fails:
table.create(),column_family.create(),update_table(),app_profile.create(), androw.commit()all run before thetry ... finally:block (line 342). If any of those calls fail or time out afterbigtable_instance.create()(line 285),finally:is never reached and the Bigtable instance is leaked.- Inside
finally:,bigtable_instance.delete()is in the sametryblock afterupdate_table()andtable.delete(). If disabling the change stream or deleting the table raises an exception,bigtable_instance.delete()is skipped. prefix='yaml_bt_cdc_it_'in the function signature is unused (instance_name = 'bt-cdc-tests'is hardcoded).
How to fix:
Move all post-instance-creation setup inside try:, and wrap bigtable_instance.delete() in its own try ... except block inside finally: so instance deletion is guaranteed even if table cleanup fails:
table = bigtable_instance.table(table_id)
table_name = f'projects/{project}/instances/{instance_id}/tables/{table_id}'
try:
table.create()
# ... column_family, update_table, app_profile, row.commit(), yield ...
finally:
try:
disable_request = bigtable_table_admin.UpdateTableRequest(
table=table_pb.Table(name=table_name),
update_mask=field_mask_pb2.FieldMask(paths=['change_stream_config']))
bigtable_client.table_admin_client.update_table(
request=disable_request).result(timeout=500)
table.delete()
except Exception:
_LOGGER.warning('Failed to clean up Bigtable CDC table [%s]', table_id, exc_info=True)
try:
_LOGGER.info('Deleting Bigtable CDC instance [%s]', instance_id)
bigtable_instance.delete()
except Exception:
_LOGGER.exception('Failed to delete Bigtable CDC instance [%s]', instance_id)| /** Used only for integration tests. Unsafe to use in production. */ | ||
| @VisibleForTesting | ||
| ReadChangeStream withEndTime(Instant endTime) { | ||
| public ReadChangeStream withEndTime(Instant endTime) { |
There was a problem hiding this comment.
Do not expose withEndTime / end_at as a public user-facing option:
withEndTime is intentionally package-private and @VisibleForTesting ("Used only for integration tests. Unsafe to use in production."). Cloud Bigtable's low watermark is not guaranteed to be monotonic (it can go backwards), so terminating a change stream at endTime can lead to data loss—which is why withEndTime was purposefully disabled as a public option on BigtableIO.ReadChangeStream.
Making withEndTime public (which also triggers ErrorProne [VisibleForTesting] warnings when called from production code in another package) and exposing end_at / end_at_timestamp in the public ReadFromBigtableCDC YAML configuration exposes an unsafe API to users.
How to fix:
Revert BigtableIO.java and remove end_at_timestamp / end_at from BigtableChangeStreamReadConfiguration and standard_io.yaml.
| BigtableIO.ReadChangeStream readChangeStream = | ||
| BigtableIO.readChangeStream() | ||
| .withProjectId(configuration.getProjectId()) | ||
| .withInstanceId(configuration.getInstanceId()) | ||
| .withTableId(configuration.getTableId()); |
There was a problem hiding this comment.
Missing metadata table and ExistingPipelineOptions configuration:
BigtableChangeStreamReadConfiguration exposes change_stream_name and app_profile_id, but omits two sets of options needed in production Bigtable CDC pipelines:
existing_pipeline_options(withExistingPipelineOptions):BigtableIO.readChangeStream()defaults toExistingPipelineOptions.FAIL_IF_EXISTS. If a user configureschange_stream_namein YAML and restarts their pipeline, the job will fail on startup unlessRESUME_OR_NEW(orSKIP_CLEANUP) can be configured.- Metadata table configuration (
withMetadataTableProjectId,withMetadataTableInstanceId,withMetadataTableTableId,withMetadataTableAppProfileId,withCreateOrUpdateMetadataTable): Production deployments commonly store the metadata table in a separate Bigtable instance or use a transactional read-write app profile separate from a read-only source table/app profile.
How to fix:
Add optional configuration fields for existing_pipeline_options and metadata_table_* to BigtableChangeStreamReadConfiguration, wire them to BigtableIO.ReadChangeStream here, and expose their mappings in standard_io.yaml.
| .addNullableRowField("value_input", VALUE_SCHEMA) | ||
| .build(); | ||
|
|
||
| static final Schema CHANGE_STREAM_MUTATION_SCHEMA = |
There was a problem hiding this comment.
flatten: true breaks row-level atomicity and loses intra-MutateRow entry ordering:
In Bigtable, a MutateRow applies a list of mutations atomically to a single row, and per the MutateRowRequest contract, entries within a MutateRow are applied in order so later entries mask earlier ones (for example, deleteRow() / deleteFamily() followed by setCell() in an atomic row overwrite).
ChangeStreamMutation intentionally groups all entries of a row mutation together to preserve both row-level atomicity and entry ordering. Splitting mutation.getEntries() into independent PCollection<Row> elements breaks row-level atomicity and—because all entries in the same ChangeStreamMutation share the exact same key, commit_timestamp, and token with no sequence index—loses intra-mutation ordering across any downstream shuffle (so a DeleteFamily followed by a SetCell in the same MutateRow can be replayed in reverse order downstream).
How to fix (two options):
- (Preferred) Remove
flattenmode: Always outputCHANGE_STREAM_MUTATION_SCHEMA(one BeamRowperChangeStreamMutationwith nestedentriesasARRAY<ROW>), which preserves both row-level atomicity and entry ordering. - If
flattenis needed for YAML convenience: Defaultflattentofalse(opt-in rather than opt-out), and add an.addInt32Field("entry_index")(the 0-based index withinmutation.getEntries()) toFLATTENED_CHANGE_STREAM_MUTATION_SCHEMAso downstream consumers can reconstruct the intra-mutation execution order.
| - pipeline: | ||
| type: chain | ||
| transforms: | ||
| - type: ReadFromBigtableCDC | ||
| config: | ||
| project: "{BT_CDC[PROJECT]}" | ||
| instance: "{BT_CDC[INSTANCE]}" | ||
| table: "{BT_CDC[TABLE]}" | ||
| app_profile: "{BT_CDC[APP_PROFILE]}" | ||
| start_at: "{BT_CDC[START_AT]}" | ||
| end_at: "{BT_CDC[END_AT]}" | ||
| flatten: true |
There was a problem hiding this comment.
Why bigtable_cdc.yaml times out here (and how to test this instead):
In BigtableIO.readChangeStream(), DetectNewPartitionsDoFn (DNP) and ReadChangeStreamPartitionDoFn (RCSP) are designed to run concurrently on a streaming runner: RCSP streams partitions and writes partition watermarks to the metadata table, while DNP tracks the global low watermark across all partitions in the metadata table and waits for all RCSPs to finish before terminating.
The Python YAML integration_tests.py harness runs external xlang transforms on Python's local batch FnApiRunner (fn_runner.py), which executes stages sequentially rather than concurrently—it buffers DNP's output in watermark_pending_inputs at MAX_TIMESTAMP and never starts RCSP until DNP finishes, creating a deadlock.
Since end_at shouldn't be exposed publicly on ReadFromBigtableCDC anyway (and FnApiRunner cannot run concurrent streaming SDF stages, which is also why ReadFromSpannerCDC and ReadFromPubSub don't run read pipelines in this harness), please remove bigtable_cdc.yaml / temp_bigtable_change_stream_table here and add a Java SchemaTransform integration test on a streaming runner instead (similar to SpannerChangeStreamsSchemaTransformIT / existing Bigtable CDC Java ITs).
Adds YAML support for reading Google Cloud Bigtable change streams.
This PR:
Tested with:
Successful run: https://github.com/apache/beam/actions/runs/35729484761
Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:
addresses #123), if applicable. This will automatically add a link to the pull request in the issue. If you would like the issue to automatically close on merging the pull request, commentfixes #<ISSUE NUMBER>instead.CHANGES.mdwith noteworthy changes.See the Contributor Guide for more tips on how to make review process smoother.
To check the build health, please visit https://github.com/apache/beam/blob/master/.test-infra/BUILD_STATUS.md
GitHub Actions Tests Status (on master branch)
See CI.md for more information about GitHub Actions CI or the workflows README to see a list of phrases to trigger workflows.